{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center><br>\n",
    "## Открытый курс по машинному обучению. Сессия № 3\n",
    "<img src=\"https://article.images.consumerreports.org/c_lfill,w_1920,ar_32:11/prod/content/dam/CRO%20Images%202017/Cars/March/CR-Cars-Hero-Used-Car-Sales-03-17\">\n",
    "\n",
    "\n",
    "### <center> Автор материала: Юлия Климушина"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## <center> Прогноз цен на подержанные автомобили </center>\n",
    "### <center> Индивидуальный проект по анализу данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В этом проекте мы будем решать задачу восстановления регрессии. Данные, используемые в этом проекте, можно скачать [тут](https://www.kaggle.com/orgesleka/used-cars-database). Датасет содержит информацию о подержанных автомобилях, выставленных на продажу на Ebay в марте-апреле 2016. Данные представлены на немецком языке. \n",
    "Цель исследования: создание модели, предсказывающую цену автомобиля на вторичном рынке. Такая модель может помочь: \n",
    "- владельцу авто, желающему продать своего железого коня, не продешевить;\n",
    "- покупателю не переплатить;\n",
    "- диллеру, занимающемуся перепродажей машин, определить насколько выгодно конкретное предложение, за какую цену  можно перепродать автомобиль."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import seaborn as sns\n",
    "from matplotlib import pyplot as plt\n",
    "%matplotlib inline\n",
    "from sklearn.linear_model import LinearRegression, Ridge, Lasso\n",
    "from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor\n",
    "from sklearn.model_selection import cross_val_score, train_test_split\n",
    "from sklearn.model_selection import GridSearchCV, learning_curve, validation_curve, KFold\n",
    "from sklearn.preprocessing import StandardScaler, PolynomialFeatures\n",
    "from sklearn.feature_extraction.text import CountVectorizer\n",
    "from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score\n",
    "from xgboost import XGBRegressor \n",
    "from scipy.sparse import csr_matrix, hstack\n",
    "import warnings\n",
    "warnings.filterwarnings('ignore')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "RANDOM_SEED = 17\n",
    "kf = KFold(n_splits=5, shuffle=True, random_state=RANDOM_SEED)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  1. Описание набора данных и признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Список признаков:\n",
    "- **dateCrawled**: дата и время первого просмотра объявления \n",
    "- **name** : название машины (сформировано из названия марки, модели и другой информации)\n",
    "- **seller** : кто продает ('privat' - частное лицо, 'gewerblich' - диллер)\n",
    "- **offerType** : тип предложения ('Angebot' - продажа, 'Gesuch' - покупка)\n",
    "- **price** : цена\n",
    "- **abtest** : A/B тест. Покопавшись в интернете, я выяснила, что A/B тестирование - это тактика, с помощью которой макетологи выясняют, какие заголовки объявления, текст, изображения, призывы к действию будут лучше работать для целевой аудитории.\n",
    "- **vehicleType** : тип авто ('coupe', 'suv', 'kleinwagen', 'limousine', 'cabrio', 'bus', 'kombi', 'andere' - \"купе\", \"внедорожник\", \"миниавто\", \"седан\", \"кабриолет\", \"автобус\", \"комби\", \"другое\", соответственно)\n",
    "- **yearOfRegistration** : в каком году машина была впервые зарегистрирована\n",
    "- **gearbox** : тип коробки передач ('manuell' - ручная, 'automatik' - автоматическая)\n",
    "- **powerPS** : мощность\n",
    "- **model** : модель\n",
    "- **kilometer** : пробег в километрах\n",
    "- **monthOfRegistration** : в каком месяце машина была впервые зарегистрирована\n",
    "- **fuelType** : тип топлива\n",
    "- **brand** : марка\n",
    "- **notRepairedDamage** : есть ли повреждения, требующие ремонта ('ja' - да, 'nein' - нет)\n",
    "- **dateCreated** : дата и время создания объявления на eBay\n",
    "- **nrOfPictures** : количество фотографий автомобиля (к сожалению, это поле везде содержит нули и поэтому бесполезно)\n",
    "- **postalCode** : почтовый индекс\n",
    "- **lastSeenOnline** : дата и время последнего просмотра объявления\n",
    "\n",
    "Целевая переменная: **price** - цена автомобиля. Перед нами стоит задача восстановления регрессии."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  2. Первичный анализ и обработка признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на данные"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "parse_dates = ['dateCrawled', 'dateCreated', 'lastSeen']\n",
    "df = pd.read_csv('data/autos.csv', sep=',', parse_dates=parse_dates, encoding = 'ISO-8859-1')\n",
    "df.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.info()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте избавимся от пропусков и не информативных признаков.\n",
    "**abtest** явно лишний признак, так как не имеет отношения к автомобилям как таковым. Насколько мне удалось выяснить, это некий показатель, используемый Ebay для определения эффективности рекламы. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Меня интересуют только объявления о продаже, поэтому я удалю строки о покупке и признак **offerType**.<br>\n",
    "Признак **vehicleType** содержит пропуски. Можно заменить их значением, обозначающим \"другое\" (andere)<br>\n",
    "Строки с пропусками в **gearbox** удалим."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.drop([ 'abtest'], axis=1, inplace=True)\n",
    "df = df[df['offerType'] != 'Gesuch'].drop('offerType', axis=1)\n",
    "\n",
    "df['vehicleType'].fillna(axis=0, value='andere', inplace=True)\n",
    "\n",
    "df.dropna(axis=0, subset=['gearbox'], inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте посмотрим на признак **brand**. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['brand'].unique()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\"sonstige_autos\" означает \"прочие автомобили\". Строк с такими значения немного и анализ показывает, что это старые и/или редкие машины, информации по которым не достаточно, чтобы строить прогноз, к тому же поле **model** у них не заполнено, поэтому удалим такие строки."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В признаке **model** 13433 пропусков, зато **brand** всегда заполнен. Учитывая, что **name** часто содержит в себе информацию о марке и модели, достанем модель оттуда. Строки, которые не подойдут под алгоритм, удалим. После этой операции удалим переменную **name**, он нам больше не пригодится."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = df[df['brand'] != \"sonstige_autos\"]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model_nan_idx = df[pd.isnull(df[\"model\"])].index\n",
    "def model_extractor(x):\n",
    "    x = x.apply(str.lower)\n",
    "    name = x['name'].split(sep='_')\n",
    "    try:\n",
    "        if name[0] == x[\"brand\"]:\n",
    "            return name[1]\n",
    "        else: return np.nan\n",
    "    except:\n",
    "        return np.nan\n",
    "\n",
    "df.loc[model_nan_idx, 'model'] = df.loc[model_nan_idx][['name', 'brand']].apply(model_extractor, axis=1) \n",
    "df.dropna(axis=0, subset=['model'], inplace=True)\n",
    "df.drop('name', axis=1, inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Разберемся с топливом **fuelType**. Заменим пустующие значения 'andere' ('другое'). "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['fuelType'].value_counts(dropna=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['fuelType'].fillna(axis=0, value='andere', inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Признак **notRepairedDamage** имеет 56335 пропусков. Можно исходить из предположения, что если владелец не упомянул в объявлении про повреждения, то он продает её как не требующую ремонта. Пометим такие пропуски как 'nein' и приведем к бинарному формату."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['notRepairedDamage'].fillna(value='nein', inplace=True)\n",
    "df['notRepairedDamage'] = df['notRepairedDamage'].map({'ja': 1, 'nein': 0}).astype('int64')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на статистику."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.describe(include='all').T"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Основные выводы:\n",
    "**nrOfPictures** по нулям. Удаляем этот признак.\n",
    "в **price** наблюдаются большие выбросы (10 в восьмой многовато даже для Bloodhound SSC)\n",
    "**kilometer** - имеет скошенное влево распределение"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.drop('nrOfPictures', axis=1, inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  3. Визуальный анализ признаков. Особенности данных."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на разброс значений количественных признаков: цены, года первой регистации и мощности."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feats = ['price', 'yearOfRegistration', 'powerPS']\n",
    "fig, axes = plt.subplots(ncols=len(feats), nrows=1, figsize=(18,6))\n",
    "\n",
    "for i, feat in enumerate(feats):\n",
    "    sns.boxplot(df[feat], ax=axes[i], orient='v', width=0.5, color='g');\n",
    "    axes[i].set_ylabel('')\n",
    "    axes[i].set_title(feat)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Из-за выбросов ничего не разобрать. Начнем с цен, определим пороги отсечения выбросов. Просмотр объявлений о продаже показали, что старая машина (15-20 лет) может стоить в районе 100 евро. В качестве верхней границы возьмем 150000. Именно столько стоят Porsche, которых не так уж мало в наборе.   "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = df[(df['price'] >= 100) & (df['price'] <= 150000)]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на год первой регистрации. Это важный признак, т.к. возраст автомобиля один из ключевых факторов, влияющих на его цену. Очевидно, что год регистрации не может быть позднее, чем год размешения объявлений. Объявления размещены в марте и апреле 2016, поэтому 2016-й год также не будем рассматривать. 17141 автомобилей зарегистрированы в 2016 и позднее. Удалим эти строки и машины старее 1976 года, то есть оставляем период в 40 лет."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = df[(df['yearOfRegistration'] >= 1976) & (df['yearOfRegistration'] < 2016)]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В выборке есть достаточно автомобилей марки Porsche, мощность двигателей которых может превышать 500 л.с. Также есть авто марки Fiat с мощность движка не превышающим 30 лошадок. Возьмем ннтервал допустимых значений (20, 600). И построим ящики с усами."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = df[(df['powerPS'] > 20) & (df['powerPS'] < 600)]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from matplotlib.ticker import FuncFormatter\n",
    "feats = ['price', 'yearOfRegistration', 'powerPS']\n",
    "\n",
    "fig, axis = plt.subplots(ncols=3, figsize=(18, 6))\n",
    "for i, feat in enumerate(feats):\n",
    "    sns.boxplot(np.log(df[feat]), ax=axis[i], orient='v', width=0.5, color='g');\n",
    "    y_formatter = FuncFormatter(lambda x, pos: ('%i')%(np.exp(x)))\n",
    "    axis[i].yaxis.set_major_formatter(y_formatter)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Признак **monthOfRegistration** целочисленный, но для модели не имеет смысла сравнивать январь с сентябрем, поэтому переквалифицируем его в категориальный. Месяц 0 будем рассматривать, как \"не определено\". Как видно из гистограммы с марта по июль публикуется больше объявлений о продаже авто, чем в остальные месяцы."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['monthOfRegistration'] = df['monthOfRegistration'].astype('object')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(10, 5))\n",
    "sns.countplot(df['monthOfRegistration']); "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на распределение количественных и бинарных признаков."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.hist(figsize=(15, 10));"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Доля машин с пробегом 150000 км: ', df[df['kilometer'] == 150000].shape[0] / df.shape[0])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Обратим внимание на пробег. Медиана и максимум = 150000. 65% машин имеют пробег 150000 км и мы имеем скошенное распределение (длинный левый хвост). У распределений целевой переменной **price** и мощности **powerPS** видим длинный правый хвост. Попробуем преобразовать данные, с тем, чтобы приблизить их распределения к нормальному. Для этого пробег возведем в степень, а **price** и **powerPS** - логарифмируем."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, axes = plt.subplots(ncols=3, figsize=(15,5))\n",
    "(df['kilometer'] ** 2).hist(ax=axes[0]);\n",
    "np.log1p(df['price']).hist(ax=axes[1]);\n",
    "np.log1p(df['powerPS']).hist(ax=axes[2]);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Изобразим матрицу корреляции"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pal = sns.light_palette(\"green\", as_cmap=True)\n",
    "fig, ax = plt.subplots(figsize=(12,8))\n",
    "sns.heatmap(df.corr(), cmap=\"RdBu_r\", annot=True, fmt = '.2f', ax=ax);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Price** положительно коррелирует с **yearOfRegistration** и **powerPS** и отрицательно - с **kilometer**. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на некоторые категориальные признаки по отдельности."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feats = ['seller', 'vehicleType', 'gearbox', 'fuelType']\n",
    "fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(14,12))\n",
    "idx = 0\n",
    "for i in range(2): \n",
    "    for j in range(2):\n",
    "        sns.countplot(df[feats[idx]], ax=axes[i][j]); \n",
    "        idx += 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Оказывается у нас только одна запись с **seller** == 'gewerblich', так что можно удалить этот признак. fuelType отличный от diesel и benzin объединим в общую группу andere."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.drop('seller', axis=1, inplace=True)\n",
    "df['fuelType'].replace(['lpg', 'hybrid', 'cng', 'elektro'], 'andere', inplace=True)\n",
    "sns.countplot(df['fuelType']);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на взаимодействие категориальных признаков и цены."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, axis = plt.subplots(nrows=1, ncols=2, figsize=(14, 6))\n",
    "sns.boxplot(x='fuelType', y='price', data=df, ax=axis[0]);\n",
    "sns.boxplot(x='gearbox', y='price', data=df, ax=axis[1]);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Автомобили с дизельными двигателями дороже бензиновых и прочих, а машины с автоматической коробкой дороже, чем с ручной. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, axis = plt.subplots(figsize=(16, 8), )\n",
    "sns.boxplot(x='brand', y='price', data=df);\n",
    "axis.set_xticklabels(df['brand'].unique(), rotation=80);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Из этой картинки следует, что Porsche существенно дороже остальных марок."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Признак **postalCode** мы исключим. Если бы рассматривался российский или, к примеру, американский рынок, то имело бы смысл поработать с ним, так как цены от области к области (от штата к штату) варьируются. На Камчатке и Аляске они, вероятно, выше, чем по стране. Но поскольку речь идет о Германии, то отбросим этот признак."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.drop(['postalCode'], axis=1, inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Предобработка данных "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y = np.log1p(df['price'])\n",
    "X = df.drop(['price'], axis=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Разделим данные на тренировочную и тестовую части и применим dummy-кодирование к категориальным признакам. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train, X_test, y_train, y_test = \\\n",
    "            train_test_split(X, y, test_size=0.3, shuffle=True, stratify=X['brand'], random_state=RANDOM_SEED)\n",
    "split_idx = X_train.shape[0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for cat_feature in X.columns[X.dtypes == 'object']:\n",
    "    X_train[cat_feature] = X_train[cat_feature].astype('category')  \n",
    "    X_train[cat_feature].cat.set_categories(X_train[cat_feature].unique(), inplace=True)\n",
    "    X_test[cat_feature] = X_test[cat_feature].astype('category')\n",
    "    X_test[cat_feature].cat.set_categories(X_train[cat_feature].unique(), inplace=True)\n",
    "    \n",
    "# В тестовом наборе оказались модели, которых не было в тренировочном, появившиеся NaN замняем \"andere\" (\"другое\").\n",
    "X_test['model'].fillna(value='andere', inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train = pd.get_dummies(X_train, columns=X_train.columns[X_train.dtypes == 'category'])\n",
    "X_test = pd.get_dummies(X_test, columns=X_test.columns[X_test.dtypes == 'category'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train.shape, X_test.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Построение базовых моделей. Выбор метрик качества. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте построим и сравним линейную модель и случайный лес. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Удаляем даты\n",
    "X_train_base = X_train.drop(['dateCrawled','dateCreated', 'lastSeen'], axis=1)\n",
    "X_test_base = X_test.drop(['dateCrawled','dateCreated', 'lastSeen'], axis=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Базовая модель линейной регрессии"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Отмасштабируем признаки"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scaler = StandardScaler().fit(X_train_base)\n",
    "X_train_scaled = scaler.transform(X_train_base)\n",
    "X_test_scaled = scaler.transform(X_test_base)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "lr = LinearRegression()\n",
    "lr.fit(X_train_scaled, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В качестве метрик качества линейной регрессии выберем MAE за его интерпретируемость. Также посмотрим на коэффициент детерминации, или коэффициент $R^2$. Данная мера качества — это нормированная среднеквадратичная ошибка. Чем она ближе к единице, тем лучше модель объясняет данные."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_preds_lr = lr.predict(X_test_scaled)\n",
    "print('LinearRegression:')\n",
    "print('\\tMAE: ', mean_absolute_error(y_test, y_preds_lr))\n",
    "print('\\tR2: ', r2_score(y_test, y_preds_lr))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Базовая модель случайного леса"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "rf = RandomForestRegressor(random_state=RANDOM_SEED)\n",
    "rf.fit(X_train_base, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_preds = rf.predict(X_test_base)\n",
    "print('RandomForestRegressor:')\n",
    "print('\\tMAE: ', mean_absolute_error(y_test, y_preds))\n",
    "print('\\tR2: ', r2_score(y_test, y_preds))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Target range: (%.2f, %.2f) ' % (y.min(), y.max()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Учитывая, что целевая переменная принимает значения в диапазоне (4.62, 11.51), то ошибка выглядит допустимой."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Создание новых признаков и описание этого процесса"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Новые признаки, которые предположительно могут коррелировать с целевым: \n",
    "- **adUpDays** - сколько дней висело объявление   \n",
    "- **kilPerYear** - среднегодовой пробег."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "new_feats_train = pd.DataFrame(index=X_train.index)\n",
    "new_feats_test = pd.DataFrame(index=X_test.index)\n",
    "\n",
    "new_feats_train['adUpDays'] = (X_train['lastSeen'] - X_train['dateCrawled']).dt.days + 1\n",
    "new_feats_test['adUpDays'] = (X_test['lastSeen'] -  X_test['dateCrawled']).dt.days + 1\n",
    "\n",
    "new_feats_train['age'] = X_train['dateCrawled'].apply(lambda x: x.year) - X_train['yearOfRegistration']\n",
    "new_feats_test['age'] = X_test['dateCrawled'].apply(lambda x: x.year) - X_test['yearOfRegistration']\n",
    "\n",
    "new_feats_train['kilPerYear'] = X_train['kilometer'] / new_feats_train['age']\n",
    "new_feats_test['kilPerYear'] = X_test['kilometer'] / new_feats_test['age']\n",
    "\n",
    "X_train.drop(['dateCrawled','dateCreated', 'lastSeen'], axis=1, inplace=True)\n",
    "X_test.drop(['dateCrawled','dateCreated', 'lastSeen'], axis=1, inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "new_feats_train['adUpDays'].hist();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "new_feats_train['kilPerYear'].hist();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Применим к признакам со смещенным распределением логарифмирование."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "new_feats_train['kilPerYear_log'] = np.log1p(new_feats_train['kilPerYear'])\n",
    "new_feats_test['kilPerYear_log'] = np.log1p(new_feats_test['kilPerYear'])\n",
    "\n",
    "new_feats_train['powerPS_log'] = np.log1p(X_train['powerPS'])\n",
    "new_feats_test['powerPS_log'] = np.log1p(X_test['powerPS'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Добавление новых признаков и полиномов второй степени улучшило качество линейной модели, но оно всё же хуже, чем у базового случайного леса, поэтому сосредоточимся на последнем. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scaler = StandardScaler().fit(X_train[['kilometer', 'yearOfRegistration']])\n",
    "features_scaled_train = scaler.transform(X_train[['kilometer', 'yearOfRegistration']])\n",
    "features_scaled_test = scaler.transform(X_test[['kilometer', 'yearOfRegistration']])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "poly = PolynomialFeatures(2)\n",
    "X_train_poly = poly.fit_transform(np.concatenate([new_feats_train[['powerPS_log', 'kilPerYear_log', 'adUpDays']],\n",
    "                                                features_scaled_train], axis=1))\n",
    "X_test_poly = poly.transform(np.concatenate([new_feats_test[['powerPS_log', 'kilPerYear_log', 'adUpDays']],\n",
    "                                                features_scaled_test], axis=1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_new = np.concatenate([X_train_poly, X_train.drop(['powerPS', 'kilometer',  \\\n",
    "                                                           'yearOfRegistration'], axis=1)], axis=1)\n",
    "X_test_new = np.concatenate([X_test_poly, X_test.drop(['powerPS', 'kilometer',  \\\n",
    "                                                       'yearOfRegistration'], axis=1)], axis=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "lr = LinearRegression()\n",
    "lr.fit(X_train_new, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_preds_lr = lr.predict(X_test_new) \n",
    "print('LinearRegression:')\n",
    "print('\\tMAE: ', mean_absolute_error(y_test, y_preds_lr))\n",
    "print('\\tR2: ', r2_score(y_test, y_preds_lr))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Модель случайного леса немного улучшилась при добавлении **adUpDays**. Добавление **kilPerYear** никак не повлияло на качество."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_new = pd.concat([X_train, new_feats_train[['kilPerYear']]], axis=1)\n",
    "X_test_new = pd.concat([X_test, new_feats_test[['kilPerYear']]], axis=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "rf = RandomForestRegressor(random_state=RANDOM_SEED)\n",
    "rf.fit(X_train_new, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_preds = rf.predict(X_test_new)\n",
    "print('RandomForestRegressor:')\n",
    "print('\\tMAE: ', mean_absolute_error(y_test, y_preds))\n",
    "print('\\tR2: ', r2_score(y_test, y_preds))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_new = pd.concat([X_train, new_feats_train[['adUpDays']]], axis=1)\n",
    "X_test_new = pd.concat([X_test, new_feats_test[['adUpDays']]], axis=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "rf = RandomForestRegressor(random_state=RANDOM_SEED)\n",
    "rf.fit(X_train_new, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_preds = rf.predict(X_test_new)\n",
    "print('RandomForestRegressor:')\n",
    "print('\\tMAE: ', mean_absolute_error(y_test, y_preds))\n",
    "print('\\tR2: ', r2_score(y_test, y_preds))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Какие же признаки оказались наиболее важными для модели случайного леса. Как видно из графика ниже, наиболее важным признаком оказался год регистрации, второй по важности признак - мощность двигателя, третий - **notRepairedDamage**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "features = X_train_new.columns\n",
    "importances = rf.feature_importances_\n",
    "num_to_plot = 10\n",
    "indices = np.argsort(importances)[::-1][:num_to_plot]\n",
    " \n",
    "plt.figure(figsize=(14,6))\n",
    "plt.title('Feature impotances for a RandomForestRegressor model', size=15)\n",
    "bars = plt.bar(range(num_to_plot), importances[indices], align=\"center\")\n",
    "ticks = plt.xticks(range(num_to_plot), features.values[indices[:]], rotation=70, size=13)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Построение кривых валидации"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Попробуем улучшить результат случайного леса. Для начала посмотрим как ведут себя кривые валидации при изменении основных параметров.\n",
    "\n",
    "Начнем с количества деревьев:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def valid_curves_plot(hyperparam, param_range):\n",
    "    param_range = param_range\n",
    "    train_scores, test_scores = validation_curve(RandomForestRegressor(random_state=RANDOM_SEED), \n",
    "                                X_train_new, y_train, param_name=hyperparam, param_range=param_range, \n",
    "                                cv=kf, n_jobs=-1, scoring='r2')\n",
    "\n",
    "    train_scores_mean = np.mean(train_scores, axis=1)\n",
    "    train_scores_std = np.std(train_scores, axis=1)\n",
    "    test_scores_mean = np.mean(test_scores, axis=1)\n",
    "    test_scores_std = np.std(test_scores, axis=1)\n",
    "    plt.figure(figsize=(9,6))\n",
    "    plt.title('Validation curves for a RandomForestRegressor model')\n",
    "    plt.xlabel(hyperparam)\n",
    "    plt.ylabel('R2')\n",
    "    plt.ylim(0.8, 1.0)\n",
    "    plt.fill_between(param_range, train_scores_mean - train_scores_std, \\\n",
    "                     train_scores_mean + train_scores_std, alpha=0.2, color=\"r\")\n",
    "    plt.plot(param_range, train_scores_mean, label='Training error', color=\"r\")\n",
    "    plt.fill_between(param_range, test_scores_mean - test_scores_std, \\\n",
    "                     test_scores_mean + test_scores_std, alpha=0.2, color=\"g\")\n",
    "    plt.plot(param_range, test_scores_mean, label='Validation error', color=\"g\")\n",
    "\n",
    "    plt.legend(loc=\"best\")\n",
    "    plt.xticks(param_range)\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "valid_curves_plot(hyperparam='n_estimators', param_range=[5, 10, 15, 20, 30, 50, 75, 100])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видно при достижении 30 деревьев точность модели на тесте выходит на асимптоту. \n",
    "\n",
    "Давайте посмотрим какие параметры регуляризации добавить в модель, чтобы недопустить переобучение.\n",
    "Посмотрим как ведет себя модель в зависимости от параметра максимальной глубины – `max_depth`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "valid_curves_plot(hyperparam='max_depth', param_range=[3, 5, 7, 9, 11, 13, 15, 17, 20, 22, 24])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видим, строить деревья глубиной более 22 смысла не имеет, качество на тесте выходит на ассиптоту.<br> \n",
    "<br>\n",
    "Построим кривые валидации для параметра `min_samples_leaf`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "valid_curves_plot(hyperparam='min_samples_leaf', param_range=[1, 3, 5, 7, 9, 11, 13])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как мы видим на тесте максимальное качество достигается, если минимальном числе объектов в листе 3. <br>\n",
    "<br>\n",
    "Параметр `max_features` определяет количество случайных признаков из `n`  исходных. Для задач регрессии рекомендуется использовать $\\frac{n}{3}$. Давайте определим оптимальный параметр для нашего случая."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "valid_curves_plot(hyperparam='max_features', param_range=[50, 100, 200, 300, 400, 500, 600, 700])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**max_features** = 200 - оптимальный вариант."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Кросс-валидация, подбор параметров"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Сделаем инициализацию параметров, по которым хотим сделать полный перебор\n",
    "parameters = {'max_features': [100,200, 300, 400, 500], \n",
    "              'min_samples_leaf': [1, 3, 5, 7], \n",
    "              'max_depth': [13, 15, 17, 20, 22, 24]}\n",
    "rfc = RandomForestRegressor(n_estimators=30, random_state=RANDOM_SEED, n_jobs=-1)\n",
    "gcv = GridSearchCV(rfc, parameters, n_jobs=-1, cv=kf)\n",
    "gcv.fit(X_train_new, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gcv.best_estimator_, gcv.best_score_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Прогноз для тестовой выборки"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Обучим случайный лес с оптимальными гиперпараметрами и получим прогноз для тестовой выборки. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "gcv.best_estimator_.fit(X_train_new, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_preds = gcv.best_estimator_.predict(X_test_new)\n",
    "print('RandomForestRegressor:')\n",
    "print('\\tMAE: ', mean_absolute_error(y_test, y_preds))\n",
    "print('\\tR2: ', r2_score(y_test, y_preds))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Нам удалось выиграть \"аж\" 0.01 на обоих метриках."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Оценка модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Переведем целевой признак обратно в километры и посмотрим насколько хорошо наша модель отработала на тестовом наборе. Для начала сравним реальные и предсказанные цены по нескольким примерам."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_test_preds = pd.DataFrame({'y_true': np.exp(y_test) - 1,\n",
    "                           'y_preds': np.exp(y_preds) - 1})\n",
    "y_test_preds['y_true'] =round(y_test_preds['y_true'], 2)\n",
    "y_test_preds['y_preds'] =round(y_test_preds['y_preds'], 2)\n",
    "y_test_preds['diff'] = np.absolute(y_test_preds['y_true'] - y_test_preds['y_preds'])\n",
    "y_test_preds.head(20).T"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видим модель довольно сильно ошибается в некоторых случаях, но в целом закономерности в данных выявлены и результат выглядит неплохо. На графике ниже видим рассеяние реальной цены vs. предсказанной цены относительно линии идентичности (красная линия). Хорошо видно, что чем больше цена, тем сильнее ошибается модель и видно тенденцию к недооценке. Очевидно это связано с недостаточностью данных по дорогим автомобилям.  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from ggplot import *\n",
    "\n",
    "ggplot(y_test_preds, aes(x='y_true',y='y_preds')) + \\\n",
    "    geom_point(color='blue')  + \\\n",
    "    xlim(-1e03,  1e+05) + ylim(-1e03, 1e+05) + \\\n",
    "    ggtitle(\"RandomForestRegressor model\") + \\\n",
    "    xlab(\"True price\") + ylab(\"Predicted price\") + \\\n",
    "    geom_abline(intercept=0, size=2, color='red')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Также видим в самом начале какие-то сильные выбросы. Посмотрев на данные, становится ясно, что модель предсказывает более высокую стоимость машин, выставленных за \"бесценок\". Возможно продавцы указывают низкую цену, чтобы привлечь покупателей, а может забыли указать, что машина требует ремонта. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_test_preds.sort_values('y_true').head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Построение кривых обучения"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте построим кривые обучения для всего объема данных."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "\n",
    "plt.figure(figsize=(12, 8))\n",
    "plt.title(\"Learning curves for a RandomForestRegressor model\")\n",
    "plt.xlabel(\"Training examples\")\n",
    "plt.ylabel(\"MSE\")\n",
    "train_sizes, train_scores, test_scores = \\\n",
    "            learning_curve(RandomForestRegressor(max_depth=24,\n",
    "                                                max_features=200,\n",
    "                                                min_samples_leaf=1,\n",
    "                                                n_estimators=30, n_jobs=-1,\n",
    "                                                random_state=RANDOM_SEED), \n",
    "                            np.concatenate([X_train, X_test]),\n",
    "                            np.concatenate([y_train, y_test]),\n",
    "                            cv=kf,\n",
    "                            train_sizes=np.linspace(0.1, 1.0, 5),\n",
    "                            random_state=RANDOM_SEED)\n",
    "train_scores_mean = np.mean(train_scores, axis=1)\n",
    "train_scores_std = np.std(train_scores, axis=1)\n",
    "test_scores_mean = np.mean(test_scores, axis=1)\n",
    "test_scores_std = np.std(test_scores, axis=1)\n",
    "plt.grid()\n",
    "\n",
    "plt.fill_between(train_sizes, train_scores_mean - train_scores_std,\n",
    "                 train_scores_mean + train_scores_std, alpha=0.1,\n",
    "                 color=\"r\")\n",
    "plt.fill_between(train_sizes, test_scores_mean - test_scores_std,\n",
    "                 test_scores_mean + test_scores_std, alpha=0.1, color=\"g\")\n",
    "plt.plot(train_sizes, train_scores_mean, 'o-', color=\"r\",\n",
    "         label=\"Training error\")\n",
    "plt.plot(train_sizes, test_scores_mean, 'o-', color=\"g\",\n",
    "         label=\"Validation error\")\n",
    "plt.legend()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Большое расстояние между кривыми указывает на переобучение. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Выводы "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Сырые данные содержали много пропусков и выбросы. Нам потребовалось провести значительную обработку и фильтрацию. К категориальным признакам мы применили one-hot encoding. Целевой признак имел сильно скошенное распределение, поэтому мы применили к нему логарифмическое преобразование.  \n",
    "\n",
    "Мы сравнили две модели и пришли к выводу, что линейная модель с полиномиальными признаками дает MAE: 0.31 и $R^2$: 0.85, в то время как случайный лес \"из коробки\" сразу выдал MAE:  0.28 и $R^2$: 0.87. К сожалению, нам не удалось синтезировать признаки, улучшающие этот результат случайного леса. Настройка гиперпараметров привела к незначительному росту качества: MAE:  0.27 и $R^2$: 0.88. \n",
    "\n",
    "Учитывая, что случайный лес показал неплохие результаты, с моей стороны было бы упущением не попровать бустинг на наших данных. Я воспользовалась питоновской реализацией XGBoost, понастраивала параметры с помощью hyperopt, но значимых улучшений не получила, поэтому решила не докучать читателям и опустила выкладки. \n",
    "\n",
    "Таким образом можно заключить, что применительно к этой задаче случайный лес сработал хорошо.\n",
    "\n",
    "Исходя из кривых обучения можно сделать вывод, чтобы улучшить модель можно:\n",
    "- увеличить размер выборки;\n",
    "- понизить сложность модели. Этого можно добиться, если использовать меньший набор признаков или увеличить регуляризацию (к примеру, можно снизить глубину деревьев или увеличить минимальном число объектов в листе)."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.2"
  },
  "widgets": {
   "state": {},
   "version": "1.1.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
